/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator.docs.constraints;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import maxbe.rel2xml.generator.Table;
import maxbe.rel2xml.generator.docs.DataType;

import org.apache.log4j.Logger;

/**
 * Class for generating values by selecting entries from a list of foreign key
 * values
 * 
 * @author Max Bechtold
 * 
 */
public class References implements ValueConstructor {

	private Logger log = Logger.getLogger(References.class);

	/** If true, the referenced values will be delivered in random order */
	private final boolean random;

	/** States whether this ValueConstructor delivers only unique values */
	private final boolean unique;

	/**
	 * States whether the foreign key this ValueConstructor belongs to reflects
	 * the dependency between the connected tables
	 */
	private final boolean dependent;

	/** Identifies the composite foreign key this column is part of */
	private int compositeForeignKeyId = 0;

	/** The list of values to be referenced */
	private List<RefValue> refValues;

	/**
	 * Determines the position in the list of referenced values (or each
	 * context) where referencing will start
	 */
	private int startIndex;

	/** A field holding the current index in refValues for in-order referencing */
	private int currentIndex;

	/**
	 * The table whose tuples are referenced by (per context) randomly chosen
	 * refValues (note the double reference)
	 */
	private Table refContext;

	/** The number of tuples that may be referenced per refContext context */
	private int contextSize;

	/** The index in refValues where the current context starts */
	private int offset = -1;

	/**
	 * Counts the entries of refValues that are removed while referencing
	 * refValues of a certain context
	 */
	private int removed;

	/**
	 * The factors of the referenced table that define the contextSizes (if
	 * refContext != null && refContext.fillfactor.varies())
	 */
	private int[] contextSizes;
	private int contextIndex;

	private Random rand;
	private static long seed = 121467198;
	private static long statSeed = 663574819;

	private static final List<DataType> types;

	static {
		types = new ArrayList<DataType>(Arrays.asList(DataType.values()));
	}

	public References(boolean random, boolean dependent, boolean unique,
			int startFrom, Table refContext) {
		this.random = random;
		this.dependent = dependent;
		this.unique = unique;
		this.startIndex = startFrom - 1;
		this.refContext = refContext;
		this.currentIndex = startIndex;
		rand = new Random(seed++);
	}

	@Override
	public boolean lengthVaries() {
		return false;
	}

	@Override
	public int getLength() {
		return -1;
	}

	@Override
	public int getSubLength() {
		return -1;
	}

	public boolean isRandom() {
		return random;
	}

	public boolean isUnique() {
		return unique;
	}

	public boolean isDependent() {
		return dependent;
	}

	public int getStartFrom() {
		return startIndex + 1;
	}

	public Table getRefContext() {
		return refContext;
	}

	public int getCompositeForeignKeyId() {
		return compositeForeignKeyId;
	}

	public void setRefContext(Table refContext) {
		this.refContext = refContext;
	}

	public void setContextSize(int contextSize) {
		if (contextSize < startIndex + 1) {
			String msg = "StartIndex for reference too high.";
			log.error(msg);
			throw (new ValueGenerationException(msg));
		}
		this.contextSize = contextSize;
	}

	public void setContextSizes(int[] contextSizes) {
		for (int contextSize : contextSizes)
			if (contextSize < startIndex + 1) {
				String msg = "StartIndex for reference too high.";
				log.error(msg);
				throw (new ValueGenerationException(msg));
			}
		this.contextSizes = contextSizes;
	}

	public void setCompositeForeignKeyId(int compositeForeignKeyId) {
		this.compositeForeignKeyId = compositeForeignKeyId;

		// Special care to be taken for independent composite foreign key
		// columns
		if (compositeForeignKeyId > 0 && !dependent) {
			rand = new Random(statSeed + compositeForeignKeyId);
		}
	}

	public void setRefValues(List<String> refValues, int factor) {
		if (unique)
			this.refValues = RefValue.generateList(refValues, 1);
		else if (dependent)
			this.refValues = RefValue.generateList(refValues, factor);
		else
			this.refValues = RefValue.generateList(refValues, -1);
	}

	public void setRefValues(List<String> refValues, int[] factors) {
		if (unique)
			this.refValues = RefValue.generateList(refValues, 1);
		else if (dependent)
			this.refValues = RefValue.generateList(refValues, factors);
		else
			this.refValues = RefValue.generateList(refValues, -1);
	}

	@Override
	public boolean needsAnnounce() {
		return unique || refContext == null;
	}

	@Override
	public void announce(int number) {
		if (unique && number > refValues.size()) {
			String msg = "Not enough reference values provided for generating unique references.";
			log.error(msg);
			throw (new ValueGenerationException(msg));
		}

		if (refContext == null) {
			offset = 0;
			contextSize = refValues.size();
			if (dependent && !random)
				currentIndex = startIndex;
			else
				currentIndex = startIndex - 1;

		}
	}

	@Override
	public void mark(int number) {
		if (refContext != null) {
			if (offset == -1)
				offset = 0;
			else
				offset += contextSize - removed;
			removed = 0;

			if (contextSizes != null)
				contextSize = contextSizes[contextIndex++];

			if (dependent && !random)
				currentIndex = offset + startIndex;
			else
				currentIndex = offset + startIndex - 1;
		}

		if (unique && number > contextSize - startIndex) {
			String msg = "Not enough reference values provided for generating unique references per context.";
			log.error(msg);
			throw (new ValueGenerationException(msg));
		}
	}

	@Override
	public String next() {
		String next = refValues.get(nextIndex()).getReference();
		return next;
	}

	@Override
	public List<String> next(int number) {
		ArrayList<String> result = new ArrayList<String>();

		if (dependent && !random)
			while (number > 0) {
				if (!refValues.get(currentIndex).hasMore())
					currentIndex = nextIndex();
				int rest = refValues.get(currentIndex).whatIsLeft();
				if (rest > number)
					rest = number;

				result.addAll(refValues.get(currentIndex).getReference(rest));
				number -= rest;
			}
		else
			for (int i = 0; i < number; i++)
				result.add(next());

		return result;
	}

	private int nextIndex() {
		if (random) {
			currentIndex = offset + startIndex
					+ rand.nextInt(contextSize - startIndex - removed);
			while (!refValues.get(currentIndex).hasMore()) {
				refValues.remove(currentIndex);
				removed++;
				currentIndex = offset + startIndex
						+ rand.nextInt(contextSize - startIndex - removed);
			}
		} else if (dependent) {
			while (!refValues.get(currentIndex).hasMore())
				currentIndex = (currentIndex < offset + contextSize - removed ? ++currentIndex
						: offset + startIndex);
		} else {
			currentIndex = (currentIndex < offset + contextSize - removed - 1 ? ++currentIndex
					: offset + startIndex);
		}
		return currentIndex;
	}

	@Override
	public String toString() {
		return (random ? "random " : "")
				+ (dependent ? "dependent " : "")
				+ (unique ? "unique " : "")
				+ "reference "
				+ (startIndex > 0 ? "starting from tuple " + startIndex + 1
						+ " " : "")
				+ (refContext != null ? "per " + refContext.getName() : "");
	}

	@Override
	public List<DataType> getSupportedDataTypes() {
		return types;
	}

}

/**
 * Class encapsulating reference values and the number of references to it
 * 
 * @author Max Bechtold
 * 
 */
class RefValue {

	/** The value to be referenced */
	private String value;

	/** The number of references still possible, -1 means unlimited */
	private int available;

	public RefValue(String value, int available) {
		this.value = value;
		this.available = available;
	}

	public boolean hasMore() {
		return available != 0;
	}

	public int whatIsLeft() {
		return available;
	}

	public String getReference() {
		if (available > -1) {
			if (available == 0) {
				throw (new ValueGenerationException(String.format(
						"Trying to reference value '%s' too often.", value)));
			}

			available--;
		}
		return value;
	}

	public List<String> getReference(int number) {
		if (available > -1) {
			if (available - number < 0) {
				throw (new ValueGenerationException(String.format(
						"Trying to reference value '%s' too often.", value)));
			}

			available -= number;
		}

		return Collections.nCopies(number, value);
	}

	public static List<RefValue> generateList(List<String> values, int factor) {
		List<RefValue> refValues = new ArrayList<RefValue>(values.size());
		for (String value : values) {
			refValues.add(new RefValue(value, factor));
		}
		return refValues;
	}

	public static List<RefValue> generateList(List<String> values, int[] factors) {
		List<RefValue> refValues = new ArrayList<RefValue>(values.size());
		Iterator<String> it = values.iterator();

		for (int factor : factors) {
			refValues.add(new RefValue(it.next(), factor));
		}
		return refValues;
	}

	@Override
	public String toString() {
		return String.format("%s (#%d)", value, available);
	}

}